Skip to content

feat(think): add opt-in, read-only HTTP fetch tool - #1821

Merged
threepointone merged 2 commits into
mainfrom
think-fetch-tool
Jun 27, 2026
Merged

feat(think): add opt-in, read-only HTTP fetch tool#1821
threepointone merged 2 commits into
mainfrom
think-fetch-tool

Conversation

@threepointone

@threepointone threepointone commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in, read-only HTTP fetch capability for Think agents via a new @cloudflare/think/tools/fetch export and a fetchTools property on Think. Disabled by default.

This addresses #1344 — letting agents read allowlisted web pages and APIs directly, without standing up a separate browsing service, while keeping strong Workers-grounded safety guarantees.

createFetchTools() generates:

  • a generic, allowlisted fetch_url tool (when a public allowlist is provided), and
  • one fetch_<name> tool per named service-binding / Fetcher target.

Using per-target named tools (instead of one polymorphic tool) gives the model a clear, unambiguous surface and lets each binding carry its own base URL, allowlist, and fixed headers.

Safety model (Workers-grounded)

  • GET-only. Mutations are out of scope for v1; they belong in dedicated action tools.
  • SSRF defenses. Blocks private / loopback / link-local / CGNAT / ULA hosts, *.internal and *.localhost, and IP literals in decimal/octal/hex/shorthand forms. URL normalization + rejection of embedded credentials.
  • Allowlist-aware redirects. Each hop is re-checked against the allowlist; cross-origin hops strip credentials/headers. Bare-origin patterns (https://example.com) auto-expand to cover all subpaths.
  • Bounded responses. Separate limits for download (maxBytes), model context (maxModelChars), and a response: "workspace" spill so large bodies land in the workspace instead of the model context.
  • Header safety. Model headers go through an allowlist; fixed binding headers take precedence over model-provided ones.
  • Markdown-first. Configurable (and disablable) default Accept header so content-negotiating endpoints return clean markdown/text/JSON instead of HTML.
  • Observability. Emits a tool:fetch event for an egress audit trail.

Hardening

  • Allowlist globs are precompiled to RegExp once at config time (no per-request recompilation).
  • Unconsumed response bodies are cancelled on non-2xx and redirect paths to avoid dangling streams.
  • Empty bodies (e.g. 204) are treated as successful empty reads rather than unsupported_content_type.
  • The onEvent callback is wrapped so a throwing observability hook can't break tool execution.

Example

Integrated into the assistant example (kitchen-sink) with workspace spill enabled and a persona hint guiding when to prefer fetch_url over the rendered-page browsing tools.

Test plan

  • pnpm exec vitest run --config src/tests/vitest.config.ts fetch-tools — 49 passing (unit + Think integration)
  • pnpm exec nx run think:build
  • pnpm run check (sherif + export checks + oxfmt + oxlint + typecheck across 113 projects)

Covered: allowlist matching + bare-origin normalization, SSRF rejections across IP formats, redirect policy + cross-origin header stripping, size limits + workspace spill, header precedence, default Accept behavior, empty/204 handling, and Think integration (tool registration gated on fetchTools, capability-prompt advertisement).

Docs

  • packages/think/README.md, docs/think/index.md — usage, safety model, allowlist semantics, fetch vs Browser Run vs RPC guidance, exports/config tables.
  • design/think.md — design rationale; design/test-coverage-matrix.md — new coverage row.
  • Changeset: .changeset/think-fetch-tool.md (@cloudflare/think minor).

Made with Cursor


Open in Devin Review

Adds a new `@cloudflare/think/tools/fetch` export and a `fetchTools`
property on `Think` that lets agents read allowlisted HTTP resources
(docs, APIs) directly. Disabled by default.

`createFetchTools()` generates a generic, allowlisted `fetch_url` tool
plus one `fetch_<name>` tool per named service-binding/`Fetcher` target,
so the model gets a clear per-target surface instead of one polymorphic
tool.

Safety model (Workers-grounded):
- GET-only; mutations are out of scope for v1.
- SSRF defenses: blocks private/loopback/link-local/CGNAT/ULA hosts,
  `*.internal` and `*.localhost`, and IP literals in decimal/octal/hex/
  shorthand forms; URL normalization; rejects embedded credentials.
- Allowlist-aware redirect policy with cross-origin header stripping;
  bare-origin patterns auto-expand to cover all subpaths.
- Separate size limits for download (`maxBytes`), model context
  (`maxModelChars`), and a `response: "workspace"` spill for large
  bodies.
- Model header allowlist; fixed binding headers take precedence over
  model-provided ones.
- Markdown-first default `Accept` (configurable / disablable) so
  content-negotiating endpoints return clean text instead of HTML.
- `tool:fetch` observability event for an egress audit trail.

Hardening: allowlist globs are precompiled to RegExp at config time;
unconsumed response bodies are cancelled on non-2xx and redirect paths.

Integrated into the `assistant` example (kitchen-sink) with workspace
spill enabled. Includes Worker-runtime unit + Think integration tests
(`fetch-tools.test.ts` + `agents/fetch-tools.ts` fixture), README/docs/
design updates, test-coverage-matrix row, and a changeset.

Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Jun 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 937319d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cloudflare/think Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jun 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@1821

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@1821

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@1821

create-think

npm i https://pkg.pr.new/create-think@1821

hono-agents

npm i https://pkg.pr.new/hono-agents@1821

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@1821

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@1821

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@1821

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@1821

commit: 937319d

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Open in Devin Review

Comment thread packages/think/src/tools/fetch.ts
Comment thread packages/think/src/tools/fetch.ts
Comment thread packages/think/src/tools/fetch.ts
The WHATWG URL parser serializes `[::ffff:127.0.0.1]` to hex form
`[::ffff:7f00:1]`, which the dotted-decimal-only regex never matched,
so IPv4-mapped IPv6 addresses bypassed our own private-network check
(the Workers egress layer still blocked them, but the defense-in-depth
layer had a gap). Decode the trailing two hextets back into dotted IPv4
and reuse the v4 rules. Adds test coverage for both mapped forms.

Co-authored-by: Cursor <cursoragent@cursor.com>
@threepointone
threepointone merged commit de6a695 into main Jun 27, 2026
4 checks passed
@threepointone
threepointone deleted the think-fetch-tool branch June 27, 2026 12:05
@github-actions github-actions Bot mentioned this pull request Jun 27, 2026
jonastemplestein added a commit to iterate/iterate that referenced this pull request Jul 13, 2026
## Summary

- keep secret streams fully public: any caller may append any event, and
every `secret/updated` event without material clears retained material
- bind each new ciphertext to its project, normalized secret path,
canonical complete origin policy, and committed event offset with
AES-GCM AAD
- require replacement material and its complete egress policy in the
same `secret.update`, using compare-and-append plus re-encryption on
offset contention
- fence refreshes on both sides of provider I/O so a stale request
cannot mint or resurrect material after any intervening update
- constrain platform-backed refresh credentials to their provider
endpoints and result origins, including GitHub App identity and
installation ownership
- replace credential-bearing bare fetches with bounded manual redirects
that revalidate every hop and reject cross-origin redirects
- reconstruct terminal responses so URL-path secrets cannot escape
through `Response.url`, navigation headers, or runtime error metadata
- intentionally invalidate legacy secret ciphertext/checkpoints;
production secret material will be wiped rather than migrated

## Security properties

Secret event types are not privileged. Public events may change
metadata, egress, or refresh configuration and may destroy availability,
but they cannot create usable material. The reducer owns the committed
offset stored beside ciphertext; copied or replayed ciphertext fails
authentication in a different event, project, path, or effective-origin
context.

A material write must explicitly name its complete egress policy. It
predicts the next event offset, encrypts for that exact context, and
compare-appends. Losing a race discards the envelope and re-encrypts
against a fresh offset. A refresh additionally records the reducer-owned
update offset observed by the request, revalidates it before provider
I/O, and compare-appends the result afterward.

Credential requests follow at most five redirects. Each hop is manual
and revalidated; cross-origin redirects are rejected before another
credential-bearing request is sent. Terminal responses are rebuilt with
empty fetch provenance and without URL-bearing navigation headers. Fetch
and malformed-redirect errors become stable sanitized responses.

## Research

- Cloudflare Request documentation:
https://developers.cloudflare.com/workers/runtime-apis/request/
- Cloudflare Response documentation:
https://developers.cloudflare.com/workers/runtime-apis/response/
- Cloudflare Workers RPC documentation:
https://developers.cloudflare.com/workers/runtime-apis/rpc/
- Cloudflare Think implementation:
https://github.com/cloudflare/agents/blob/762998da1c873701305a44c598e9c029617047b4/packages/think/src/tools/fetch.ts#L418-L606
- Think redirect hardening PR:
cloudflare/agents#1821

Think uses the same core shape—manual bounded redirects, per-hop
validation, request reconstruction, and credential/header removal across
origins. This patch uses the smaller, stricter policy appropriate for
secret material: credential-bearing cross-origin redirects are rejected.

## Verification

- `pnpm format`
- `pnpm typecheck`
- `pnpm lint` (zero warnings)
- `pnpm test` (OS: 1,189 passed; 1 skipped; all other workspace suites
passed)
- focused crypto/reducer/platform/config tests
- 8 live-worker e2e tests covering public event replay, explicit
material policy, both refresh race windows, cross-origin redirects,
URL-path response concealment, and project egress
substitution/interception

<!-- CLOUDFLARE_PREVIEW -->
## Environment Config Lease

<!-- CLOUDFLARE_PREVIEW_STATE -->
<!--
{
  "apps": {
    "os": {
      "appDisplayName": "OS",
      "appSlug": "os",
      "status": "deployed",
      "updatedAt": "2026-07-13T11:40:41.320Z",
      "headSha": "b78f30f2e60d8ad992f2950c440c6d7b26944d80",
      "message": null,
      "publicUrl": "https://os.iterate-preview-6.com",
"runUrl":
"https://github.com/iterate/iterate/actions/runs/407068669292171",
      "shortSha": "b78f30f",
      "deployDurationMs": 80088,
      "testDurationMs": 175652,
"testRetries": "3 retried: DESIRED: a live-capability fetch handler
serves WebSockets at an app host (vitest x1) · delivery expressions must
end in a property step, rejected before commit (vitest x1) · runs
\"scheduler-agent-checkin\" through the project REPL (specs x1)",
      "workerSizeKib": 16082.86,
      "workerGzipKib": 4338.82,
      "mainWorkerGzipKib": 4335.47
    },
    "auth": {
      "appDisplayName": "Auth",
      "appSlug": "auth",
      "status": "deployed",
      "updatedAt": "2026-07-13T11:37:45.885Z",
      "headSha": "b78f30f2e60d8ad992f2950c440c6d7b26944d80",
      "message": null,
      "publicUrl": "https://auth.iterate-preview-6.com",
"runUrl":
"https://github.com/iterate/iterate/actions/runs/407068669292171",
      "shortSha": "b78f30f",
      "deployDurationMs": 17650,
      "testDurationMs": 214,
      "testRetries": null,
      "workerSizeKib": 4032.01,
      "workerGzipKib": 819.66,
      "mainWorkerGzipKib": null
    },
    "streams-example-app": {
      "appDisplayName": "Streams Example App",
      "appSlug": "streams-example-app",
      "status": "deployed",
      "updatedAt": "2026-07-13T11:38:04.341Z",
      "headSha": "b78f30f2e60d8ad992f2950c440c6d7b26944d80",
      "message": null,
      "publicUrl": "https://streams.iterate-preview-6.com",
"runUrl":
"https://github.com/iterate/iterate/actions/runs/407068669292171",
      "shortSha": "b78f30f",
      "deployDurationMs": 13830,
      "testDurationMs": 18668,
      "testRetries": null,
      "workerSizeKib": 4890.81,
      "workerGzipKib": 1399.72,
      "mainWorkerGzipKib": null
    }
  },
  "environmentConfigLease": {
    "dopplerConfig": "preview_6",
    "leasedUntil": 1784029547191,
    "leaseId": "486ab6b4-f050-4c1c-b04b-bc9fdbacf4eb",
    "slug": "preview-6",
    "type": "environment-config-lease"
  },
  "notice": null
}
-->
<!-- /CLOUDFLARE_PREVIEW_STATE -->

<details>
<summary>Lease: preview-6 | Doppler config: preview_6 | Type:
environment-config-lease | Leased until:
2026-07-14T11:45:47.191Z</summary>

| app | status | commit | preview | size (gzip) | deploy duration | test
duration | retries | cleanup duration | workflow run | updated | summary
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---
|
| Auth | deployed | `b78f30f` |
[https://auth.iterate-preview-6.com](https://auth.iterate-preview-6.com)
| 819.7 KiB | 17.6s | 214ms | | | [Workflow
run](https://github.com/iterate/iterate/actions/runs/407068669292171) |
2026-07-13T11:37:45.885Z | |
| OS | deployed | `b78f30f` |
[https://os.iterate-preview-6.com](https://os.iterate-preview-6.com) |
4.24 MiB (+3.3 KiB vs main) | 80.1s | 175.7s | 3 retried: DESIRED: a
live-capability fetch handler serves WebSockets at an app host (vitest
x1) · delivery expressions must end in a property step, rejected before
commit (vitest x1) · runs "scheduler-agent-checkin" through the project
REPL (specs x1) | | [Workflow
run](https://github.com/iterate/iterate/actions/runs/407068669292171) |
2026-07-13T11:40:41.320Z | |
| Streams Example App | deployed | `b78f30f` |
[https://streams.iterate-preview-6.com](https://streams.iterate-preview-6.com)
| 1.37 MiB | 13.8s | 18.7s | | | [Workflow
run](https://github.com/iterate/iterate/actions/runs/407068669292171) |
2026-07-13T11:38:04.341Z | |

</details>
<!-- /CLOUDFLARE_PREVIEW -->

<!-- loc-report -->
| Group | Lines | Significant |
| --- | ---: | ---: |
| Product | +676 -165 | +557 -144 🟩🟩🟩🟩🟥 |
| Tests | +736 -29 | +670 -29 🟩🟩🟩🟩🟩 |
| Docs | +52 -2 | +49 -2 🟩⬜⬜⬜⬜ |
| Generated | +55 -27 | +25 -13 🟩⬜⬜⬜⬜ |
| **Total** | **+1,519 -223** | **+1,301 -188** 🟩🟩🟩🟩🟥 |

<sub>Lines counts every changed line; Significant ignores blank lines
and JS comments. Between 71e99e5 and b78f30f, bucketed first-match-wins
into the groups defined in `scripts/ci/loc-report.ts`.</sub>
<!-- /loc-report -->
abstractedfox pushed a commit to abstractedfox/agents that referenced this pull request Jul 23, 2026
* feat(think): add opt-in, read-only HTTP fetch tool

Adds a new `@cloudflare/think/tools/fetch` export and a `fetchTools`
property on `Think` that lets agents read allowlisted HTTP resources
(docs, APIs) directly. Disabled by default.

`createFetchTools()` generates a generic, allowlisted `fetch_url` tool
plus one `fetch_<name>` tool per named service-binding/`Fetcher` target,
so the model gets a clear per-target surface instead of one polymorphic
tool.

Safety model (Workers-grounded):
- GET-only; mutations are out of scope for v1.
- SSRF defenses: blocks private/loopback/link-local/CGNAT/ULA hosts,
  `*.internal` and `*.localhost`, and IP literals in decimal/octal/hex/
  shorthand forms; URL normalization; rejects embedded credentials.
- Allowlist-aware redirect policy with cross-origin header stripping;
  bare-origin patterns auto-expand to cover all subpaths.
- Separate size limits for download (`maxBytes`), model context
  (`maxModelChars`), and a `response: "workspace"` spill for large
  bodies.
- Model header allowlist; fixed binding headers take precedence over
  model-provided ones.
- Markdown-first default `Accept` (configurable / disablable) so
  content-negotiating endpoints return clean text instead of HTML.
- `tool:fetch` observability event for an egress audit trail.

Hardening: allowlist globs are precompiled to RegExp at config time;
unconsumed response bodies are cancelled on non-2xx and redirect paths.

Integrated into the `assistant` example (kitchen-sink) with workspace
spill enabled. Includes Worker-runtime unit + Think integration tests
(`fetch-tools.test.ts` + `agents/fetch-tools.ts` fixture), README/docs/
design updates, test-coverage-matrix row, and a changeset.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(think): block IPv4-mapped IPv6 addresses in SSRF guard

The WHATWG URL parser serializes `[::ffff:127.0.0.1]` to hex form
`[::ffff:7f00:1]`, which the dotted-decimal-only regex never matched,
so IPv4-mapped IPv6 addresses bypassed our own private-network check
(the Workers egress layer still blocked them, but the defense-in-depth
layer had a gap). Decode the trailing two hextets back into dotted IPv4
and reuse the v4 rules. Adds test coverage for both mapped forms.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Sunil Pai <18808+threepointone@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
abstractedfox added a commit to abstractedfox/agents that referenced this pull request Jul 23, 2026
Reconstitute work on this repo

Add index.py as default entry point

Start of basic package support

This is the beginning of work on this feature, it's a little comment-heavy
to that end and has some patterns that may not be permanent (these have
generally been noted as such). As of this commit, there are tests that
start to poke at this, namely the one that brings in fastapi, which currently
fails due to an extension it doesn't like.

Python package proof of concept

Proof of concept for package support in dynamic Python workers.
As of this commit, it can retrieve an sdist with a flat layout from pypi and
install it into the virtual filesystem. It doesn't do dependency resolution,
so the current tests fail when attempting to import FastAPI's deps.

Clarifying comment

Use wheels properly

Used sdists before, now it uses wheels. Pre cleanup commit

Wheel changeover cleanup

Use simpler packages for minimal package support test

This test only needs to confirm that retrieval of a hard list of pure python packages, without
dependency resolution, works.

Move python logic out of 'bundle' path

This seems to be specific to JS packages

Slight cleanup, remove test

More cleanup

Re-add parameter that was mistakenly deleted

Revert "remove dependency that was causing issues"

This dependendency caused issues when working in a container, but its removal
was purely for my sake

This reverts commit 4f8990dded1887a32a4a5b8f1ed3843d982914fe.

Order deps alphabetically

Remove unused variable

Make createWorker Python return more uniform with JS

Make createWorker registry pattern more consistent with JS

Also makes PyprojectToml interface less stringent

Small fixes

These got caught by the checks done by `pnpm run check`

Style fixes

Remove parsing for version string

Comment dep version parameter

Compensate for distribution packages' names not matching their imports

Remove default values from test

These are already set implicitly by createWorker

Move Python package metadata collection into its own function

Move Py deps installation into its own function

Account for type checker

Nested dependency installation without version resolution

Undo unintended compat date change

Bump up compatibility date (test)

Prevent adding python and JS deps to the same worker

Implicitly add workers-runtime-sdk package

Remove check for when there are no dependencies, since there will always
be at least one

Add python_workers flag if user-supplied flags exclude it

Typo

Clarifying comment

Include installWarnings with returned object

Type checker fix

Change tests to assert existence of compat flags

Remove redundant compat flag default

This is implicit on Python dynamic workers now

Mark comment as TODO

Change installDependenciesPython to replicate the JS impl's usage of the result object

Finish propagating `result` object and explicitly disable pyproject.toml and package.json in one worker

Add more TODOs

Change return type

feat(think): add opt-in, read-only HTTP fetch tool (#1821)

* feat(think): add opt-in, read-only HTTP fetch tool

Adds a new `@cloudflare/think/tools/fetch` export and a `fetchTools`
property on `Think` that lets agents read allowlisted HTTP resources
(docs, APIs) directly. Disabled by default.

`createFetchTools()` generates a generic, allowlisted `fetch_url` tool
plus one `fetch_<name>` tool per named service-binding/`Fetcher` target,
so the model gets a clear per-target surface instead of one polymorphic
tool.

Safety model (Workers-grounded):
- GET-only; mutations are out of scope for v1.
- SSRF defenses: blocks private/loopback/link-local/CGNAT/ULA hosts,
  `*.internal` and `*.localhost`, and IP literals in decimal/octal/hex/
  shorthand forms; URL normalization; rejects embedded credentials.
- Allowlist-aware redirect policy with cross-origin header stripping;
  bare-origin patterns auto-expand to cover all subpaths.
- Separate size limits for download (`maxBytes`), model context
  (`maxModelChars`), and a `response: "workspace"` spill for large
  bodies.
- Model header allowlist; fixed binding headers take precedence over
  model-provided ones.
- Markdown-first default `Accept` (configurable / disablable) so
  content-negotiating endpoints return clean text instead of HTML.
- `tool:fetch` observability event for an egress audit trail.

Hardening: allowlist globs are precompiled to RegExp at config time;
unconsumed response bodies are cancelled on non-2xx and redirect paths.

Integrated into the `assistant` example (kitchen-sink) with workspace
spill enabled. Includes Worker-runtime unit + Think integration tests
(`fetch-tools.test.ts` + `agents/fetch-tools.ts` fixture), README/docs/
design updates, test-coverage-matrix row, and a changeset.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(think): block IPv4-mapped IPv6 addresses in SSRF guard

The WHATWG URL parser serializes `[::ffff:127.0.0.1]` to hex form
`[::ffff:7f00:1]`, which the dotted-decimal-only regex never matched,
so IPv4-mapped IPv6 addresses bypassed our own private-network check
(the Workers egress layer still blocked them, but the defense-in-depth
layer had a gap). Decode the trailing two hextets back into dotted IPv4
and reuse the v4 rules. Adds test coverage for both mapped forms.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Sunil Pai <18808+threepointone@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

feat(think): tool-call lifecycle hook follow-ups (#1343) (#1823)

* feat(think): improve tool-call lifecycle hooks (#1343 follow-ups)

Address follow-ups 1, 2, 4, and 5 from #1343:

- Preserve preliminary streaming through `beforeToolCall` for async
  generator `execute` tools, while keeping scalar tools free of synthetic
  `preliminary` chunks. Extract the shared decision logic into
  `_resolveToolCallDecision`.
- Type `ToolCallResultContext.output` (and fix `ToolCallContext.input`)
  per tool when an explicit `TOOLS` generic is passed: narrowing on
  `ctx.toolName` now narrows input/output. Default `ToolSet` behavior is
  unchanged. Add `tool-call-types.test-d.ts`.
- Add a raw `needsApproval` tool + `beforeToolCall` dual-gate ordering test.
- Add an `execute` invocation counter to the test tool and assert
  block/substitute never run it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(think): clarify synthetic preliminary chunk on block/substitute of streaming tools

Document and annotate that blocking or substituting an `async function*`
tool surfaces one extra `preliminary: true` tool-result chunk to
observation hooks (e.g. onChunk). This is inherent: the wrapper must
commit to an AsyncIterable shape synchronously to preserve streaming on
the execute path, and the AI SDK turns every yielded value into a
preliminary + final. The model-visible final output is unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Sunil Pai <18808+threepointone@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

Stabilize browser test process teardown

Improve `killProcess` in browser tests to avoid flaky Vitest worker exits during shutdown. The change detaches and destroys child stdio pipes, removes listeners, and unreferences the child before sending `SIGKILL` to the process group, preventing EPIPE/late-write races from `wrangler dev`. It also uses a guarded `done` callback with `child.once("exit")` plus an unref’d timeout fallback so cleanup resolves exactly once.

Harden browser test process teardown

Avoid test worker crashes during `wrangler dev` shutdown by removing the stdin pipe, keeping no-op error handlers attached, and only detaching the data listeners before killing the child process group. This prevents late EPIPE/ECONNRESET errors from surfacing as uncaught exceptions in vitest.

Improve probe resilience and error handling

Add retry logic and validation for transient failures against freshly-deployed workers. Introduces `withRetry()` for flaky async operations with linear backoff, `startTurn()` to validate API responses, and `waitForWsLive()` to warm up the WebSocket route before scenarios run. Improves error visibility with better error handlers and logging, and adds automatic retries in the test suite to tolerate transient blips.

fix(chat-recovery): bound Durable Object memory-limit (OOM) crash loops (#1825) (#1826)

* fix(chat-recovery): bound Durable Object memory-limit (OOM) crash loops (#1825)

A chat-recovery turn whose Durable Object isolate exceeds its 128 MB
memory limit could loop forever, re-running the (billable) turn on every
platform alarm retry. The isolate streams a little content before the
reset, which bumps the durable progress counter; on the next wake
recovery reads that as forward progress and resets both progress-keyed
bounds (maxAttempts, noProgressTimeoutMs), and because each crash lands
inside the alarm-debounce window the attempt counter is pinned too. With
maxRecoveryWork defaulting to Infinity, no instrument could ever seal the
turn, so the model ran forever.

This lands a layered fix:

1. Finite maxRecoveryWork default (1000, was Infinity). The work meter is
   the one signal that keeps climbing across the loop, so a finite default
   seals a runaway with reason="work_budget_exceeded".

2. OOM-specific in-DO budget (chatRecovery.maxOomRetries, default 3). A
   memory reset re-OOMs on re-run (the turn's working set, not the
   platform, is the cause), so it is classified as a distinct deterministic
   failure rather than a deploy-style transient: it is NOT deferred and
   retried forever. Each crash bumps a durable per-incident oomAttempts
   counter; after a small number of tries it seals with
   reason="out_of_memory". Fast and attributable.

3. Alarm-boundary circuit breaker (Agent.alarm()) as the universal
   backstop for OOMs that bypass the in-DO budgets entirely - thrown
   before the budget code runs (boot-time state hydration), or whose own
   small writes also OOM under memory pressure. Left unhandled such an
   error propagates out of alarm() and the platform auto-retries forever.
   alarm() now intercepts ONLY Durable Object memory-limit resets at the
   outermost frame, where the heavy turn has unwound and GC has reclaimed
   its footprint, so the seal/purge writes can land where mid-turn ones
   OOMed. A durable strike counter (static maxAlarmMemoryLimitStrikes,
   default 3) tolerates a few resets - backing off the looping rows so the
   retry is not a hot loop - then seals the recovery (out_of_memory) and
   surgically purges ONLY the looping schedule rows, leaving unrelated
   scheduled tasks intact. Emits a new alarm:memory_limit_reset event.
   Everything except memory-limit resets re-throws exactly as before.

Supporting changes:

- Broaden + export isDurableObjectMemoryLimitReset(error): matches the
  shared "exceeded its memory limit" fragment so truncated/reworded
  surfacings observed in real #1825 logs still classify. Sibling to
  isDurableObjectCodeUpdateReset / isPlatformTransientError.
- _executeScheduleCallback now DEFERS (re-throws) memory-limit resets for
  one-shot rows instead of swallowing them after in-process retries, so the
  error reaches the alarm-boundary breaker; track the executing row id so
  the breaker can purge the exact looping row.
- think/ai-chat override _cf_recoveryAlarmCallbacks() and
  _cf_sealMemoryLimitedRecovery() to target their recovery continuation
  callbacks and terminalize active incidents (banner + onExhausted + seal).
- Remove the redundant result-path OOM handling in continueLastTurn: those
  turns are already terminalized, so it only risked wasteful reschedules
  and duplicate terminal signals.

Adds unit + integration coverage (predicate, listActiveChatRecoveryIncidents,
alarm circuit breaker), an RFC follow-up section, docs, and changesets.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat-recovery): reset alarm OOM strike counter on a clean alarm

The alarm-boundary memory-limit strike counter (maxAlarmMemoryLimitStrikes,
only ever deleted when the breaker sealed — never after a clean alarm — so
it actually tracked LIFETIME resets. A Durable Object hitting rare,
non-consecutive transient spikes (e.g. one a month) would eventually reach
the strike budget and wrongly seal healthy recovery work.

alarm() now best-effort clears cf_agents:oom_alarm_strikes after a clean
_cf_runAlarmBody() so strikes must be consecutive to seal. The clear reads
first and only writes when a strike is recorded, so the common no-strike
path costs no write. Adds a regression test (strike recorded -> clean alarm
resets to 0 -> next OOM starts at strike 1).

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Sunil Pai <18808+threepointone@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

Streamline CI workflow execution settings

Set `NX_PARALLEL` to `2` in both PR and release workflows to cap Nx task concurrency. In the release workflow, remove the Playwright version/cache/install steps and the extra `nx run-many -t test` invocation, simplifying the publish pipeline after build/check.

Retry `changeset version` on transient GitHub API failures

The changelog generator (@changesets/changelog-github) hits the GitHub
GraphQL API, which intermittently fails with transient network errors
like "Premature close". changesets bails cleanly without writing files
on that failure, so retry up to 3 times before giving up.

Co-authored-by: Cursor <cursoragent@cursor.com>

think releases are patch

Version Packages (#1822)

* Version Packages

* raise agents floor

* Update pnpm-lock.yaml

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Sunil Pai <18808+threepointone@users.noreply.github.com>

fix(agent-tools): forward proxied sub-agent tool events stuck at input-available (#1589) (#1827)

* fix(agent-tools): forward proxied sub-agent tool events stuck at input-available (#1589)

`tailAgentToolRun` (in both `AIChatAgent` and `Think`) drained the stored
chunk backlog and only afterwards attached its live forwarder, with `await`
boundaries in between. Any chunk the child stored AND broadcast during that
window was neither in the drained snapshot nor live-forwarded, so it silently
vanished from the parent's stream — leaving tool parts (notably
`tool-output-available`) stuck at `input-available` in `useAgentToolEvents`.
A network-paced proxied remote stream (a sub-agent returning a remote
`toUIMessageStreamResponse()`) hits this window constantly; a fast local child
mostly avoids it.

The forwarder is now registered BEFORE the backlog is drained, with live
chunks buffered and replayed in order and deduped by sequence. Think also
realigns its live sequence to the true high-water mark so a post-restart
re-attach can't collide. Hardened forwarder teardown so an empty forwarder/
sequence map entry can't keep the broadcast idle-guard hot for the DO's
lifetime, and a drain/read failure detaches before surfacing.

Also documents why the per-run live sequence counter is deliberately separate
from the resumable store's chunk_index: progress/milestone frames
(`reportProgress`) are forwarded but NOT durably stored, so they depend on the
live counter — sequencing forwards off the stored count would collide them and
the tail's dedupe would silently drop them.

Tests: attach-window forwarding (#1589) and non-stored progress/milestone
forwarding through the replay->live handoff, for both ai-chat and think.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ai-chat): realign live sequence on cold-counter re-attach (post-restart)

AIChatAgent's `tailAgentToolRun` drained the stored backlog but never
realigned the in-memory live sequence counter afterwards, unlike Think.
On a re-attach after the child's Durable Object restarts or wakes from
hibernation, `_agentToolLiveSequences` is cold (empty) while the durable
backlog sits at N, and chat-recovery resumes the turn via `tailAgentToolRun`
without re-running `startAgentToolRun` (which seeds the counter). The
broadcast snoop then handed the recovered turn's new chunks sequences from 0
— all <= N — and `emit`'s high-water dedupe silently dropped every one,
leaving the parent permanently stuck with no post-restart chunks.

Realign the counter to the stored high-water mark after the drain (matching
Think). On a warm attach the counter is already in lockstep, so this is a
no-op; it only bites the cold post-restart path. Adds a deterministic
regression test that seeds a running run with a backlog, wipes the live
counter, re-attaches, and asserts a subsequent broadcast forwards at N+1
instead of being dropped.

Caught by Devin review on #1827.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ai-chat): harden agent-tool tail teardown to match Think

Three defensive gaps in AIChatAgent's `tailAgentToolRun` (all present in the
Think version) surfaced now that the forwarder is registered before the drain,
which widened the window in which a consumer can detach:

- Empty `cancel` handler left a zombie forwarder registered after a consumer
  cancelled its reader. `closed`/`forward`/cleanup are now hoisted out of
  `start` so `cancel` can reach them, and `cancel` marks the tail dead and
  detaches the forwarder.
- Unguarded `controller.close()` in `close()` (and the `emit` catch calling
  `close()`) could throw on an already-cancelled stream; that throw propagated
  out of `interceptAgentToolBroadcast`'s forward loop and starved the run's
  sibling tailers of the chunk. `controller.close()` is now guarded and the
  `emit` catch just detaches instead of closing.
- Unguarded `controller.error()` in the catch path could throw if the stream
  was already torn down; now wrapped.

Extracted a shared `detach()` (mirrors Think) so the forwarder-set cleanup is
no longer duplicated across close/error paths. Adds a regression test that
tails a run from two consumers, cancels one, and asserts the sibling still
receives a subsequent broadcast (verified it fails without the fix).

Caught by Devin review on #1827.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Sunil Pai <18808+threepointone@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

feat(examples): add sandbox-coding-agent — Think orchestrating Claude Code in containers (#1830)

RFC: CodingAgent — a new @cloudflare/coding-agent package (#1831)

* docs(design): add rfc-coding-agent (first-class CodingAgent for Think)

Proposes promoting examples/sandbox-coding-agent (#1830) into @cloudflare/think
as a supported `CodingAgent` class — a Think subclass that drives a CLI coding
agent (Claude Code first) inside a Cloudflare Sandbox, exported per-CLI as
`@cloudflare/think/claudecode`.

Locks the surface before any core code moves: an internal TurnRuntime seam in
Think (private, CodingAgent is its only consumer), a per-CLI adapter contract,
tokenless AI Gateway egress + snapshot-based durability built in, DO-tuned
recovery, and a conformance-test strategy for stream-json drift. Strategic
stance: own the public interface, keep the engine swappable (a matured
@ai-sdk/harness could become an impl detail behind the same class — #1829).

Status: proposed.
Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(design): expand rfc-coding-agent with config, threads, and seams

Folds in the firm follow-ups from review:
- §8 dynamic config (resolve-by-precedence, freeze-on-first-turn) + topology
  (standalone / Chats-child threads / orchestrated), with the "no top-level
  binding assumption" requirement.
- §9 two seams designed in from day one: a filesystem backend interface (so the
  durable cloudflare/workspace VFS can supersede snapshots later) and run/preview
  by target (container dev server vs worker-bundler + env.LOADER).
- §10 future-work pointer to a Workers-native runtime (Runtime B) behind the same
  TurnRuntime seam.
- New alternative (adopt cloudflare/workspace now — rejected, preview-only) and
  expanded decision questions.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(design): drop first-class Chats; threads are a userland directory pattern

The fixed chats_index/ChatSummary schema is the part consumers outgrow
immediately — a coding directory needs repo/branch/status/lastDiff, etc. So:

- rfc-coding-agent §8: reframe "threads" as a userland directory pattern (plain
  Agent + subAgent with domain-specific metadata), not a `Chats` base class. The
  shipped, load-bearing primitives it leans on are unchanged (subAgent + Props,
  parentAgent, RemoteContextProvider).
- rfc-think-multi-session: record the third (now-leaning) answer to open question
  #1 — don't ship a Chats base class; ship primitives + a thin client hook + an
  example.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(design): rewrite rfc-coding-agent — own package, AIChatAgent base, pluggable engine

Adversarial review reshaped the design:
- New @cloudflare/coding-agent package, NOT a Think subclass/subpath. Extends
  AIChatAgent, so onChatMessage is the seam and Think core is untouched (drops
  the riskiest piece — the turn-runtime refactor). Honors the AGENTS.md layering
  preference (containers don't belong in the chat base).
- Pluggable engine: CliEngine ships first (lift the example's mapper), HarnessEngine
  is the goal (reuses HarnessAgent's tested stream-mapping + session lifecycle;
  gated on #1829). No speculative multi-CLI adapter interface — extract after codex.
- Durability redesigned around two decoupled lifecycles (DO vs container have
  different shutdown behaviors); reconcile-on-wake; honest that claude -p can't
  resume a killed turn and that re-run can double-apply edits; bound snapshot cost.
- Egress scoped honestly (per-provider, TLS-dependent, OAuth CLIs can't be tokenless).
- Branch is mutable working state (git checkout), only repo identity is frozen.
- Filesystem VFS, preview, git ops, HITL, and Runtime B moved to "Directions"
  (not v1 seams). Added a Testing & CI section.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(design): mark package name resolved in rfc-coding-agent decision

Package name @cloudflare/coding-agent + /claude-code subpath confirmed. Engine
default, snapshot policy, and first-PR scope remain deliberately open.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Sunil Pai <18808+threepointone@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

feat(think): bundle workers-ai-provider and accept model id strings (#1832)

Make the common model-config case zero-boilerplate: `getModel()` (and the
`beforeTurn`/`beforeStep` per-turn/per-step overrides) now accept a model id
string resolved through a built-in `workers-ai-provider` instance, so users no
longer install/import/wire the provider themselves.

- getModel() returns `ThinkModel` (new exported alias for
  `LanguageModel | ThinkModelId`); `ThinkModelId` gives `@cf/...` autocomplete
  while accepting any gateway slug string.
- Bundle `workers-ai-provider` + `@ai-sdk/openai`/`@ai-sdk/anthropic` wire
  plugins as deps; `@cf/...` ids hit Workers AI (with sessionAffinity),
  other `<provider>/<model>` slugs route through AI Gateway.
- Add `getAIBinding()` (default `env.AI`) and public `resolveModel(model?)`
  for side inference calls (summarization/compaction).
- Widen `TurnConfig.model` and `StepConfig.model` to `ThinkModel`; resolve
  string overrides before handing the step to the AI SDK.
- Scaffolder + all starters + non-starter examples + docs use string models;
  drop `workers-ai-provider` from their deps and from THIRD_PARTY_DEPENDENCIES.

Co-authored-by: Sunil Pai <18808+threepointone@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

update deps

fix(agent-tools): clone run parts in reducer so StrictMode does not double text (#1835) (#1836)

`applyToRun` shallow-copied a run's parts with `[...seeded.parts]`, sharing
element references with the previous state. `applyChunkToParts` then mutated
those part objects in place (e.g. `text += delta`), leaking the mutation into
`prev`. React double-invokes setState updaters in StrictMode / dev hydration,
so each text-delta was applied twice against the same already-mutated prev,
doubling streamed text in Next.js, TanStack Start, Remix, and any
<StrictMode> app. Clone each part before mutating to keep the reducer pure.

Co-authored-by: Sunil Pai <18808+threepointone@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

fix(chat): serialize reconnect-driven resumes to stop activeResponse race (#1837) (#1838)

* fix(chat): serialize reconnect-driven resumes to stop activeResponse race (#1837)

With `resume: true` (the default), `useAgentChat` re-probes the stream from
its WebSocket `onAgentOpen` handler on every reconnect. The AI SDK's
`Chat.makeRequest` has no concurrency guard: every resume shares the single
mutable `this.activeResponse`, and its `finally` finalizer reads
`this.activeResponse.state.message` with a bare (unguarded) read before
clearing it (the adjacent `finishReason` field is optional-chained, the
`message` field is not). Under a reconnect storm (flaky mobile link, or a
Durable Object bounce on redeploy), a later resume could overwrite + clear
`activeResponse` before an earlier resume's finalizer ran, so the earlier
finalizer read `undefined` and threw a handled
`TypeError: Cannot read properties of undefined (reading 'state')`.

The previous guard did not close the window:
- `customTransport.isAwaitingResume()` only covers the handshake — it flips
  false the instant `STREAM_RESUMING` resolves, but the AI SDK only sets
  status to "submitted" in a later microtask (it sits behind
  `await transport.reconnectToStream(...)`), and
- `statusRef.current` is lagging React state that has not re-rendered yet.

So a second `open` landing in that post-handshake / pre-status-propagation
window sailed past both guards and launched an overlapping resume.

Fix: serialize re-probe resumes with `resumeInFlightRef` — never issue a new
`resumeStream()` while one is still outstanding. The flag is held for the
whole resume lifetime and force-cleared in the socket-effect cleanup so an
orphaned resume (agent swapped on a `_pk` change) can't leave the gate stuck
closed. A `resumeGenerationRef` token prevents a stale, orphaned resume's
late `.finally` from reopening the gate underneath a newer resume.

The definitive activeResponse-local fix belongs upstream in Vercel `ai`; this
stops the SDK from triggering the overlap.

Adds a deterministic regression test (`resume-overlap-race.test.tsx`) that
drives the real hook through a reconnect storm via a fake EventTarget agent
and asserts the overlapping reconnect issues no second resume and the
finalizer never reads a cleared activeResponse.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(chat): explain why onAgentClose must not reset resumeInFlightRef (#1837)

A review suggested clearing `resumeInFlightRef` in `onAgentClose` for clarity.
That is unsafe: the flag is owned by the in-flight resume and cleared only by
its own `.finally` (or invalidated via the cleanup generation bump). Resetting
it on close would set it false while the resume may still be mid-flight,
re-coupling correctness to close/open task ordering and reopening the overlap
window. It is also unnecessary — handshake-phase drops are gated by
`isAwaitingResume()` and streaming-phase drops settle `makeRequest` (which
clears the flag). Document the invariant inline so it isn't "hardened" away.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Sunil Pai <18808+threepointone@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

remove explicit Env declarations where not needed

Inline Env types in live issue repros

Adds explicit `Env` type definitions in both live repro `server.ts` files so bindings are typed without relying on generated `env.d.ts`. Updates both repro `tsconfig.json` files to stop including `env.d.ts`, and removes required secret declarations in `issue-1691-live/wrangler.jsonc` to align with optional provider API keys.

fix(think): preserve attachment fetchMetadata through messenger serialization (#1833) (#1839)

serializableMessengerEvent stripped everything except id/mediaType/name/
size/text/url from attachments before handing events to sub-agent DOs,
discarding fetch, raw, and the platform identifier. For adapters like
@chat-adapter/telegram the file id lives only in fetchMetadata.fileId and
the top-level id is never set, so attachments were irretrievable inside a
sub-agent.

Add a serialization-safe fetchMetadata field to MessengerAttachment,
preserve it through serializableMessengerEvent, and carry it through
toMessengerAttachment while backfilling the top-level id from a known
metadata key (id/fileId/mediaId/fileUniqueId).

Co-authored-by: Sunil Pai <18808+threepointone@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

Version Packages (#1828)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

Create real-icons-wonder.md

Version Packages (#1843)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

remove husky/lint-staged for now

feat(agent-think): issue repro/fix agent on Workers + Containers (#1861)

* feat(agent-think): issue repro/fix agent on Workers + Containers

A Think agent that reproduces and fixes cloudflare/agents GitHub issues
inside a container-backed @cloudflare/workspace VFS, triggered from an
issue comment (@agent-think <instruction>) via a GitHub App webhook
worker. Supersedes the CI-based /repro + /pr Actions from #1844 — same
skills, but running as a persistent Worker + pre-warmed container
instead of Actions runners.

Architecture (patterns from aron/cloudflare-workspaces-prototype):
- AgentThink WorkerEntrypoint: dispatch() RPC from the webhook worker;
  returns in ~1s (submitMessages only — container gh/git auth happens
  inside the durable turn via beforeTurn, so the caller's waitUntil
  cancellation window can never kill the run)
- ThinkAgent DO owns the Workspace (SQLite VFS) + the durable turn;
  two exec backends: container (full Linux: gh/git/npm/node/wrangler)
  and just-bash isolate for cheap text ops
- Sandbox DO hosts the Cloudflare Container; WarmPool DO keeps one
  pre-warmed and hands them out per session
- live thread UI (Vite + React) at /thread/:session
- skills (reproduce / open-pr) mounted read-only from R2; repros must
  ship a minimal Vite frontend so maintainers can click the deployed
  URL and watch the failing behavior in a UI

The worker holds no GitHub App credentials — the webhook worker mints
a short-lived installation token per dispatch.

Note: requires the enable_abortsignal_rpc compat flag — the container
backend's health probe passes an AbortSignal over cross-DO RPC.

Verified end-to-end in prod on issue #1859: trigger comment to bot
reply in 5s, 30-minute turn with real clone/install/deploy, structured
repro report posted back on the issue.

* docs(agent-think): HANDOFF — branch/PR landed, note skill-recipe verification

* docs(agent-think): gh-app no longer posts an 'on it' comment (👀 only)

* chore(agent-think): AGENTS.md, tidy root configs, standardise on .env

- Replace HANDOFF.md (session-log style) with AGENTS.md: aims, how the
  system works, the rules we hold ourselves to, and the edge cases that
  cost real debugging time (abortsignal RPC flag, lossy tail, stale
  container reconcile, R2 skill seeding, Access, WARP builds).
- Vitest configs move next to their suites (test/, tests-e2e/); root
  keeps only vite.config.ts (thread UI build).
- Env files standardise on .env + .env_example (the e2e harness reads
  .env); drop .dev.vars.example and the stray root WARP pem copy.
- Drop the vite alias workaround for agents/chat/react — the subpath
  export exists upstream now, plain resolution works.

* chore(agent-think): prune unused deps, add author

@cloudflare/worker-bundler, ws, @types/ws: imported nowhere.
@cloudflare/workers-types: redundant — tsconfig consumes the
wrangler-generated worker-configuration.d.ts runtime types only.
(isomorphic-git and @platformatic/vfs stay: optional peers of
@cloudflare/workspace whose main entry — which we bundle — imports
both; git.diff runs on isomorphic-git.)

* fix(agent-think): commit generated worker types for CI; address review

- Commit worker-configuration.d.ts (wrangler types) and stop ignoring
  it — CI has no way to generate it, so the tsconfig types reference
  failed with TS2688 on a fresh checkout.
- tsconfig extends agents/tsconfig (verbatimModuleSyntax et al.), with
  the types list overridden to the generated runtime file — keeping
  @cloudflare/workers-types alongside it would conflict. client.tsx now
  typechecks too (was outside the old include).
- compatibility_date 2026-05-26 -> 2026-06-11 (repo standard), both
  configs; types regenerated against it.
- write tool now takes the same per-file lock as edit: its
  stat-then-write mode preservation had the same interleaving window
  edit's read-modify-write guards against. Lock extracted to
  src/tools/fs/file-lock.ts.

* docs(agent-think): advertise auto-created parent dirs in the write tool

The store already mkdir -p's the parent on every write; telling the
model saves it a container exec mkdir round-trip first.

* feat(agent-think): GPT-5.5 via gateway catalog + command-center UI

Model: openai/gpt-5.5 through the default AI Gateway's model catalog
(Unified Billing over the AI binding — no provider key). The
providers: [openai] plugin is required: workers-ai-provider refuses
{provider}/{model} slugs without it (verified empirically: text, tool
calls, and streaming all work with the plugin; raw env.AI.run works
either way but Think needs an AI SDK LanguageModel).

Command center: the root URL is now a dashboard run by a singleton
CommandCenterAgent (synced-state registry of every thread + counters).
ThinkAgent reports dispatch/tool/turn events fire-and-forget —
observing must never break a run. The UI gains a ChatGPT-style left
sidebar listing threads reverse-chronologically, live over agents
state sync; /thread/:session renders inside the same shell. The old
plain-text root banner is gone (root serves the SPA, with a worker
fallback where asset-first routing is not emulated).

* feat(agent-think): command-center repo cards + sidebar search

Main screen leads with per-repo cards (name, github link, issue/status
counts) per the wireframe; the sidebar gains a search filter and a
ChatGPT-style Recents treatment. Sidebar persists across thread
navigation (unchanged).

* fix(agent-think): per-comment turn idempotency

The repo#issue idempotency key silently swallowed re-mentions: once an
issue's first turn completed, submitMessages returned the old
submission (accepted:false) and nothing ran. The key now includes the
triggering commentId (passed by gh-app); dev dispatches without one get
a random key. Webhook redeliveries are already deduped in gh-app's KV
before dispatch, so nothing is lost.

* chore(agent-think): observable command-center reporting

Log (never throw) when a lifecycle report fails, and emit one
structured line per registry update — silent-success and silent-failure
were indistinguishable in the logs.

* fix(agent-think): hermetic assets fixture for the unit suite

CI has no vite output (dist/client is not committed), so the root-route
test read an empty body. The test config now points ASSETS at a
committed fixture with the SPA root node.

* fix(agent-think): HTTP snapshot fallback for the command center

Cloudflare Access on the domain passes authenticated HTTP but eats
WebSocket upgrades (zero WS ever reached the worker — the thread view
only worked via useAgentChat's HTTP get-messages polling). Plain
useAgent state sync has no such fallback, so the command center
rendered empty. GET /api/command-center returns the registry snapshot;
the client hydrates from it and polls while the WS is not connected.

* feat(agent-think): issue title + requester avatar on thread rows

ThreadMeta carries the GitHub issue title and who mentioned
@agent-think (login + avatar). Activity rows and the sidebar show the
title; the requester's avatar sits on each row with a hover tooltip
('login: instruction'). Both flow from the webhook payload through
dispatch; old threads without the fields fall back to the instruction.

* fix(agent-think): route /agents/* and /api/* worker-first — WS upgrades died at the assets router

The assets layer forwards ordinary no-asset-match requests to the
worker but not WebSocket upgrades, so every wss:// connect to
/agents/* failed while plain HTTP worked — which is why the command
center sat on the HTTP fallback and showed 'disconnected'. (Corrects
the earlier Access diagnosis; Access passes authenticated WS fine.)

feat(agent-think): repro branches + per-PR live demo deployments in the skills (#1868)

* feat(agent-think): reproduce skill publishes the repro as a pullable branch

New step: after verification, push the repro project as an orphan
repro/issue-<n> branch on the target repo (checkout IS the runnable
project; re-runs force-push the same branch; tar-copy excludes
node_modules/dist/.wrangler/.env — the image has no rsync). The report
comment and structured result now carry reproBranchUrl so other agents
can pull exactly what was built. Best-effort: a protected-branch
rejection is reported, not fatal.

* feat(agent-think): open-pr skill ships a live demo deployment per PR

Every fix PR now includes a temporary deployment demoing the change:
build+pack the fixed package, install the tarball into a minimal Vite
demo (or reuse the repro/issue-<n> branch project), deploy --temporary,
and put the demo URL + claim link in the PR body. Also: keep examples/
docs truthful in the same PR when the change affects them. Docs-only /
no-runtime changes skip the demo with a stated reason.

* fix(agent-think): survive noisy execs; operator session reset

RCA (PLANS/agents/agent-think-1845-rca.md): a full-monorepo pnpm
install streamed ~8 min of output through the DO, OOMing the isolate;
the persisted backlog then burned the 30s CPU limit on every wake, so
recovery never ran and the session death-looped — re-kicks died in
seconds against the same wall.

- exec tool description + both skills now mandate redirect-to-file +
  tail for noisy commands (installs/builds/tests); /tmp is fine for
  logs (container-local, never synced).
- pnpm baked into the image (drops the corepack step).
- ThinkAgent.resetSession() + AgentThink.resetSession(session) RPC:
  operator escape hatch that releases the container assignment, wipes
  all durable state, and aborts the isolate — the only way to unbrick
  a poisoned session today.
- AGENTS.md edge case documented.

* fix(agent-think): install jq in the container image

The system prompt and skills advertise jq on the container backend,
but the image never installed it (runs hit '/bin/sh: 1: jq: not
found').

* feat(agent-think): 🚀 turn-alive reaction on the triggering comment

👀 (gh-app) only proves the webhook was seen; the Think turn adds 🚀
the moment it wakes (beforeTurn, direct REST with the installation
token, once per dispatch, fire-and-forget). 👀-without-🚀 now cleanly
reads as 'turn is dead'.

* feat(agent-think): model-driven 🚀 liveness reaction (replaces the hook)

Per review: the MODEL adds the 🚀 via gh in the container as its first
action — proving the whole chain (model loop, container, authed gh) is
alive, not just that the turn woke. System-prompt instruction carries
the exact comment id; 👀-without-🚀 reads as 'agent dead'.

* chore(agent-think): fall back to Kimi K2.7 while unified-billing credits are dry

Workers AI billing path; the gpt-5.5 gateway-delegate plumbing stays in
place for a one-line switch back.

* fix(agent-think): address review — non-destructive repro publish + stale step ref

The orphan-branch publish now happens in a scratch git-init dir pushed
by URL, so /workspace/repo stays intact for the root-cause hypothesis
and follow-up PR work. Fix the '(step 6)' cross-reference left stale by
renumbering.

* chore(agent-think): drop claim URLs from reports and PRs

Nobody needs the claim link — reports/PRs now say
'Repro/Demo URL (expires after 60 mins): <url>' and the structured
results lose claimUrl/demoClaimUrl.

* chore(agent-think): quality-review fixes

- AGENTS.md described the replaced beforeTurn rocket hook; it now
  documents the shipped model-driven mechanism (and what 🚀 proves).
- Step cross-references in both skills are by NAME, not number — the
  numeric ones already broke once this PR when a step was inserted.
- resetSession's abort delay is a named constant with its rationale.

* feat(agent-think): four-tool surface — read, write, edit, bash (TOOLS.md)

The model now sees exactly read/write/edit/bash, aligned with pi and
Claude Code (far more training data on 'bash' than a bespoke 'exec').
Think's workspace built-ins (list/find/grep/delete) are excluded from
the provider request via beforeTurn activeTools (~600 prompt tokens
reclaimed per call); ls/grep/rm/find happen through bash.

bash (renamed from exec, src/tools/bash.ts) also gains the pi-inspired
upgrades: optional timeout (SIGTERM + exit 124, GNU convention),
head+tail truncation per stream (the old head-only cut threw away the
tail where build errors live), per-backend guidance folded into the
backend param's schema, and a slimmed description. read's offset/limit
gain sane maxima so zod stops serializing MAX_SAFE_INTEGER noise.

TOOLS.md documents the surface, what is deliberately not exposed, the
lineage (Aron's fs-tools + pi conventions + Claude Code naming), and
the four-bar test for adding a fifth tool.

* chore(agent-think): move TOOLS.md to design/tools.md

* chore(agent-think): drop the tools design doc

The decisions live in the code comments and pi alignment speaks for
itself; the doc was already drifting risk.

feat(agent-think): GPT-5.5 with medium reasoning effort (#1878)

Unified-billing credits are topped up (verified: a live gpt-5.5 call
billed $0.000535 through the default gateway). reasoning_effort is a
first-class workers-ai-provider model setting forwarded into the
request — DelegateCallOptions' typing lags that runtime, hence the
narrow cast. Verified live through the exact provider construction:
text (reasoning_tokens > 0 at medium), tool calls, and streaming with
tools all pass. Kimi fallback stays one line away for the next credit
drought.

fix(agent-think): keep PR body outside workspace (#1879)

fix(agent-think): keep workspace files in container (#1882)

chore(agent-think): remove unused gitDiff RPC (#1885)

fix(agent-think): harden runs and simplify workspace lifecycle (#1889)

* fix(agent-think): surface terminal turn failures

* fix(agent-think): configure operating prompt as context

* fix(agent-think): scope lifecycle to active runs

* fix(agent-think): keep observer reporting non-blocking

* fix(agent-think): retain recovery trace identity

* test(agent-think): align target and terminal contracts

* fix(agent-think): preserve evicted media in durable vfs

* refactor(agent-think): unify workspace filesystem

* refactor(agent-think): simplify workspace and pool

* chore(agent-think): drop full-sync preview dependency

fix(@cloudflare/voice): Wait for voice transcriber readiness before starting calls (#1891)

* Add voice transcriber readiness RFC

* Implement voice transcriber readiness

* Clarify voice transcriber readiness RFC

* fix(voice): clean up failed startup calls

* fix(voice): guard startup cleanup races

* fix(voice): harden call startup cleanup

* fix(voice): finalize rejected call startups

* fix(voice): guard voice input startup races

* fix(voice): address transcriber readiness review

---------

Co-authored-by: Christopher Little-Savage <clittle-savage@cloudflare.com>

fix: prevent addMcpServer from reporting restored OAuth as ready (#1869)

* fix: report authenticating from addMcpServer for restored OAuth connections

A connection restored after a Durable Object wake keeps its persisted
AUTHENTICATING state, but the OAuth provider's in-memory authUrl is only
assigned during a live redirectToAuthorization() and is never rehydrated
from storage. addMcpServer's existing-connection early return required
that in-memory URL to report authenticating, so a restored connection
awaiting consent fell through to READY — disagreeing with getMcpServers()
for the same server in the same tick, and wedging callers that drive the
OAuth prompt off the return value.

addMcpServer now falls back to the persisted auth_url for authenticating
connections. If no auth URL exists in memory or storage, it re-runs the
connect flow to mint a fresh one instead of reporting READY. The HTTP
path also reuses an existing server's id (as the RPC path already did)
so re-adding a known server never orphans its storage row.

Fixes #1855

* fix: gate persisted auth URLs on OAuth state validity; re-mint stale links

Second-round hardening on top of the persisted-auth_url fallback:

- The persisted auth_url is only served while its OAuth state is still
  redeemable, validated through the provider's own checkState. The state
  nonce and code verifier expire (10 minutes for the built-in provider),
  so a URL restored after a long hibernation can be a dead link; serving
  it would wedge the flow at the callback with 'State expired'. Expired
  or unverifiable-invalid URLs now fall through to the reconnect path,
  which mints and re-persists a fresh sign-in link. The check fails open:
  URLs without a state parameter and providers without checkState are
  served as before.

- Pins the HTTP-path id-reuse behavior with a dedicated test: re-adding a
  known (name, url) whose in-memory connection is gone reuses the stored
  row's id instead of minting a fresh one and orphaning the row.

Fixes #1855

* fix: preserve restored MCP OAuth state

Restored AUTHENTICATING connections lose the provider's in-memory authUrl, so addMcpServer previously fell through to READY even while storage still reported OAuth in progress. Prefer live then persisted HTTP(S) authorization URLs, and reconnect the existing connection when neither is servable, mapping authentication, discovery, and failure outcomes without re-registering it.

fix: always apply the Worker-safe JSON Schema validator to MCP client connections (#1902)

* fix: apply Worker-safe JSON Schema validator on MCPClientManager.connect()

The RPC addMcpServer path constructs connections via connect(), which
passed `options.client ?? {}` instead of merging defaultClientOptions,
so the CfWorkerJsonSchemaValidator default was skipped. When a server
exposed tools with outputSchema, the MCP SDK fell back to AJV, whose
new Function schema compilation Workers disallows, failing discovery
with 'Code generation from strings disallowed for this context'.

Merge defaultClientOptions on connect() to match createConnection(),
keeping caller-supplied jsonSchemaValidator overrides intact.

Fixes #1893

* refactor: make MCPClientConnection own the Worker-safe validator default

Move the CfWorkerJsonSchemaValidator default into the MCPClientConnection
constructor so every construction path gets it, and route connect()
through createConnection() instead of duplicating construction — the
duplication is what let the two paths drift apart in the first place.

Validator instances can't survive JSON serialization, so registerServer()
no longer persists a caller-supplied jsonSchemaValidator, and restore
drops any serialization-degraded validator held by older rows; both fall
back to the Worker-safe default after hibernation instead of clobbering
it with a dead object and failing discovery.

codemode: echo the execution call log on the proxy tool output (#1904)

* feat(codemode): echo the durable tool-call log on the proxy tool output

* docs(codemode): document the calls field on the proxy tool output

* test: expect the calls field on the approve output

feat(mcp): allow declaring url-mode elicitation capability and handling elicitation on the MCP client (#1903)

* fix(mcp): support url-mode elicitation with a handler on the MCP client

The client hardcoded capabilities.elicitation = {} (form-mode only),
clobbering anything the caller passed, and shipped a throwing-stub
handleElicitationRequest with no injection point. Servers only send
elicitation modes advertised at the initialize handshake, so url-mode
elicitation (MCP spec 2025-11-25) was unreachable for client agents.

- New overridable Agent.onElicitRequest(request, serverId). A class
  method survives Durable Object hibernation and applies to restored
  connections, unlike a callback passed at connect time (which is why
  the handler is not a per-call addMcpServer option). Plumbed through
  MCPClientManagerOptions.elicitationHandler (scoped per connection,
  including the restore and id-migration paths) and a per-connection
  elicitationHandler option for non-Agent usage.
- Capabilities follow the handler: connections advertise
  { form: {}, url: {} } when a handler is configured and the legacy
  form-only {} otherwise, so a capability is never claimed that nothing
  can handle. Agent wires the handler only when onElicitRequest is
  actually overridden. An explicit client.capabilities.elicitation wins
  wholesale, is persisted, and survives hibernation.
- Direct handleElicitationRequest override is deprecated in favor of
  the elicitationHandler option.

Closes #1875

* test(conformance): enable the SEP-1034 client elicitation defaults scenario

ConformanceHost now overrides onElicitRequest (accepting) and declares
form.applyDefaults + url, so the SDK client applies schema defaults to
accepted form elicitations. Removes the corresponding expected-failure
baseline entry.

* docs(examples): demonstrate MCP client elicitation in the mcp-client example

The agent overrides onElicitRequest to broadcast the elicitation to
connected browser clients and await a human answer through a pending-
response map resolved by a callable respondToElicitation. The UI renders
a form generated from the request's requestedSchema (url-mode renders an
open-link card) and adds a Run button per tool so elicitation-triggering
tools can actually be exercised.

* docs(examples): add a url-mode elicitation tool to the mcp-elicitation server

* docs(examples): document the /mcp endpoint path for the elicitation demo

* fix(examples): tool args form and always-visible elicitation overlay in mcp-client

Run previously sent empty args (failing tools with required inputs) and
pending elicitations rendered at the top of the page, off-screen from
the Run button that triggered them — the tool call looked hung. Tools
now get an args form generated from their inputSchema (shared
SchemaFields renderer with the elicitation card) and elicitations render
in a fixed overlay.

* chore: format client.tsx

fix(mcp): advertise no elicitation capability when no handler is configured (#1910)

Connections without an elicitation handler advertised form-mode
elicitation while rejecting every request that arrived, so spec-
compliant servers chose elicitation over their fallback flows and the
tool call failed mid-flight. The capability is now advertised only when
it can be handled; without a handler, servers see no elicitation
capability and fall back gracefully. Handler registration is gated on
the advertisement because the SDK client throws when registering a
handler for an undeclared capability.

The deprecated instance-patch of handleElicitationRequest no longer
receives requests unless the capability is declared explicitly; RPC
tests migrated to the elicitationHandler option.

fix(mcp): configure client elicitation handler (#1911)

* fix(mcp): configure client elicitation handler

* fix(mcp): use grouped elicitation handlers

* fix(test): url-mode elicit request literal requires elicitationId

* docs(mcp): note why configureElicitationHandler rebuilds the client

Client.registerCapabilities is merge-only, so a rebuild is the only way
to un-advertise a mode when handlers are cleared before connecting.

* persist advertised MCP capabilities so restore precedes fiber recovery

Handlers are functions and cannot survive hibernation, but the capabilities
they advertise can: configuring handlers stamps them onto each stored server
row (server_options.capabilities), and the manager consumes the stamp as a
capability seed when recreating a known server. Restored connections
re-advertise the same modes at the handshake before onStart reconfigures the
handlers, so automatic MCP restore keeps its original position before
fiber/chat recovery and recovered turns see MCP tools.

The stamp is valid for one restore: any configure call re-stamps every row,
so a deploy that stops configuring handlers stops advertising stale modes
after a single wake. init() re-entry rebuilds the SDK client so reconnects
pick up handler changes. Seeding is self-contained in createConnection, so
storage restore, the Agent RPC restore, and addMcpServer re-adds all get it
uniformly.

rename configureElicitationHandler to configureElicitationHandlers (#1917)

* rename configureElicitationHandler to configureElicitationHandlers

The method takes a { form, url } group and every related name is already
plural (MCPClientElicitationHandlers, the elicitationHandlers connection
option); the manager and connection methods were the singular outliers.
The API is unreleased, so the rename ships in the same release that
introduces it.

* remove stray package-lock artifacts

* remove stray mcp-client package-lock artifact

* ignore npm package-lock artifacts

fix(voice): honor pcm16 audio sample rate (#1909)

* fix(voice): honor pcm16 audio sample rate

Add a sampleRate option to VoiceAgentOptions and include it in the audio_config message sent when a call starts. This lets servers declare the native rate of raw pcm16 output while preserving the existing 16 kHz default.

Teach VoiceClient to read sampleRate from audio_config and use it when constructing AudioBuffer instances for pcm16 playback instead of always assuming 16 kHz. Encoded formats continue to rely on browser decoding.

Add protocol and playback tests for configured 24 kHz pcm16 output and the default 16 kHz fallback, including a dedicated test Durable Object binding.

* fix(voice): reset pcm16 sample rate to default when audio_config omits it; add changeset

Co-Authored-By: Beta-Devin AI <248786709+beta-devin-ai-integration[bot]@users.noreply.github.com>

* docs(voice): document sampleRate option

Co-Authored-By: Beta-Devin AI <248786709+beta-devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Beta-Devin AI <248786709+beta-devin-ai-integration[bot]@users.noreply.github.com>

fix(mcp): defer capability seed clearing (#1925)

feat(think): add ChatOptions.metadata per-turn recovery-safe metadata carrier (#1921)

fix(think): resolve messenger self-mentions (#1920)

fix(think): label channel speakers in messenger messages (#1907)

* fix(think): support channel speaker labels

* chore: add changeset for channelSpeakerLabel feature

* fix(think): suppress speaker label for DM action events

Action events now follow the same channel-only rule as regular messages:
labelled in channels, unlabelled in DMs. The speaker label only serves to
disambiguate multiple humans, which cannot happen in a 1:1 DM.

Co-Authored-By: Beta-Devin AI <248786709+beta-devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Beta-Devin AI <248786709+beta-devin-ai-integration[bot]@users.noreply.github.com>

agent-think: Install Python in sandbox image (#1927)

Install Python alongside the existing native build toolchain so packages that fall back to node-gyp can build inside the sandbox. Keep Python TLS clients on the same trusted certificate bundle as the other container tools.

agent-think: Use team AI Gateway token (#1929)

Authenticate model calls with the team AI Gateway token and attach the agent-think project tag for spend attribution. Keep GPT-5.5 with medium reasoning as the primary model and fall back to Claude Opus 4.8 when the primary dispatch fails.

agent-think: Refresh GitHub token on continue (#1930)

Bind the command center continuation path to gh-app’s private token broker. Refresh the durable run context with a new short-lived installation token before submitting into the existing Think session, so operators can continue failed runs without another issue comment.

agent-think: Recover stale running threads (#1931)

Allow the command center to atomically reclaim a running thread after ten minutes without a lifecycle update. Show the recovery action in the UI so a submission that terminalized without reporting command-center state can be continued without creating a duplicate active turn.

fix(mcp): correct elicitation release guidance (#1933)

- remove unsupported lower-level connection promises from changesets
- fix the mcp-client example start command
- describe URL acceptance as opening the external flow, not completing it

Version Packages (#1899)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

agent-think: Isolate and recover Workspace sync (#1939)

* agent-think: Cover repository sync recovery

Exercise a local dependency install through the production container path and keep a visible running window after the durable command result. The test proves recovery can finish without replaying the command.

* agent-think: Isolate workspace durable state

* Recover exact Durable Object storage resets

* agent-think: observe durable tool completion in E2E

* agent-think: Pin Workspace PR build

fix(think): forward props through onStart wrapper (#1937)

Fixes #1936

Generated with [Devin](https://devin.ai)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

fix(agents): make turndown stub diagnostic (#1944)

The agents/vite turndown stub protects Workers bundles from just-bash's transitive turndown import, but direct app use previously failed silently by returning empty markdown.

Throw a targeted error instead and document the stubTurndown: false opt-out for applications that use turndown directly.

Refs #1901

feat(voice): add AssemblyAI streaming STT provider (#1912)

* docs: design spec for AssemblyAI voice STT provider

Spec for @cloudflare/voice-assemblyai — a type-compliant Streaming v3
STT provider for the Agents voice pipeline, mirroring the Deepgram/Telnyx
example. Captures scope (STT-only, baseUrl-configurable for AI Gateway),
the v3 protocol mapping, test plan, and deferred work (typed gateway
helper, Workers AI self-host, on-prem pharma).

* docs: simplify AssemblyAI provider spec — lean options surface

Apply design-review simplifications:
- options down to apiKey/model/formatTurns/medical/keyterms/baseUrl/params
- add `params` passthrough; drop endOfTurnConfidenceThreshold
- hardcode encoding=pcm_s16le and sample_rate=16000 (pipeline-fixed)
- drop `region` (use baseUrl for EU/gateway); document URLs in README
- cut `prompt` from v1; close() no longer awaits Termination
- single test file; add Known Limitations (no onError, model-determined language)

* docs: lock spec to u3-rt-pro and correct against AssemblyAI API reference

Verified streaming params against AssemblyAI's live API docs (via MCP):
- lock model to u3-rt-pro (the voice-agent model); drop the `model` option
- auth via Authorization header (raw key, no prefix), NOT a ?token= query param
- drop format_turns (u3-rt-pro finals are always formatted)
- fully typed surface, no passthrough: domain, keyterms, minTurnSilence,
  maxTurnSilence, interruptionDelay, continuousPartials, baseUrl
- rename medical:boolean -> domain:"medical-v1"|(string&{})
- hardcode speech_model/sample_rate/encoding; conditional params only when set
- limitations: no onError, language model-determined, single model / 6 languages

* docs: add prompt and vadThreshold options to AssemblyAI spec

Verified against AssemblyAI prompting + streaming API docs:
- prompt is a connection param (no UpdateConfiguration round-trip needed);
  it is the u3-rt-pro differentiator and the language-guidance lever. Doc the
  "omit for the optimized default prompt" recommendation.
- vadThreshold (vad_threshold): VAD silence-confidence threshold for noisy envs.
- both appended only when set; language limitation softened (guided via prompt).

* docs: add languageDetection + onLanguageDetected to AssemblyAI spec

language_detection applies to u3-rt-pro (native multilingual code-switching) and
returns language_code/language_confidence on Turn events. Since the Cloudflare
transcript callbacks are text-only, pair the flag with a provider-specific
onLanguageDetected(languageCode, languageConfidence) callback to surface it
without changing the shared interface.

* docs: implementation plan for AssemblyAI voice STT provider

* feat(voice-assemblyai): scaffold package

* feat(voice-assemblyai): options interface and URL builder

* feat(voice-assemblyai): AssemblyAISTT class skeleton + test infra

* feat(voice-assemblyai): connect via fetch upgrade with Authorization header

* feat(voice-assemblyai): map Turn events to onInterim/onUtterance

* feat(voice-assemblyai): map SpeechStarted and language metadata

* feat(voice-assemblyai): buffer audio before connect, flush on open

* feat(voice-assemblyai): graceful close with Terminate handshake

* test(voice-assemblyai): pin malformed-message robustness

* docs(voice-assemblyai): README

* docs: list @cloudflare/voice-assemblyai in voice provider docs

* feat(voice-assemblyai): speech model selection and turn-detection tuning

- Add a `speechModel` option (defaults to u3-rt-pro) with the `AssemblyAISpeechModel` type and model-aware validation that rejects u3-rt-pro-only params (prompt, continuousPartials, interruptionDelay) on universal-streaming models.
- Default min/max turn silence to 400/1280 ms (above the server's 100/1000) so speakers can pause mid-thought without the turn being cut off; callers can still override.
- Default continuous_partials on for u3-rt-pro for a live mid-turn transcript.
- Log unexpected WebSocket closes (auth/session errors only arrive as close frames) and rewrite wss:// to https:// for Cloudflare's fetch-based upgrade.

* docs(voice-assemblyai): rewrite README and link AssemblyAI docs

- Streamline the intro, usage, options table, and how-it-works sections; document the new `speechModel` option and the 400/1280 ms turn-silence defaults.
- Add an "AssemblyAI documentation" section linking the relevant streaming pages (Universal-3 Pro, turn detection & partials, message sequence, streaming API, endpoints & data zones, Medical Mode, keyterms) and a dashboard link for the API key.

* feat(examples): add AssemblyAI voice agent example

Real-time voice agent running entirely in a Durable Object: AssemblyAI u3-rt-pro streaming STT, Workers AI LLM (glm-4.7-flash) with tools, and Workers AI MeloTTS for keyless TTS. Browser mic streams 16 kHz PCM over a plain WebSocket via the useVoiceAgent React hook, with barge-in, streaming responses, and conversation memory. Only ASSEMBLYAI_API_KEY is required.

* feat(voice-assemblyai): target Universal-3.5 Pro + dynamic agent_context

Update the AssemblyAI streaming provider to the current model and API.

Plugin (voice-providers/assemblyai):
- Lock speech_model to universal-3-5-pro; drop the speechModel option and
  the old model-compat assertions (universal-3-5-pro is is_u3_pro server-side,
  so it supports prompt/agent_context/mode/etc).
- Add connection params: mode, agentContext, previousContextNTurns,
  languageCode, voiceFocus, voiceFocusThreshold, validated against the
  server's ranges (1500-char caps, threshold requires voiceFocus,
  interruptionDelay 0-1000, previousContextNTurns 0-100).
- Defer turn-silence / partials to mode instead of force-defaulting them.
- Add session.updateAgentContext(text): sends an UpdateConfiguration with
  agent_context mid-session (pre-connect buffering, latest-only, 1500 cap).

Framework (@cloudflare/voice):
- Add optional TranscriberSession.updateAgentContext?(text) (no-op for
  providers without context carryover), a forwarder on
  AudioConnectionManager, and pipeline calls so withVoice feeds each spoken
  reply/greeting back to the transcriber as conversational context.

Also update the example, provider/voice READMEs, and voice docs.

* feat(voice-assemblyai): skip agent_context updates when carryover is off

When previousContextNTurns is 0, the server discards agent_context (the
carryover window is zero), so AssemblyAISession.updateAgentContext() now
early-returns instead of sending an UpdateConfiguration the server would
throw away. Contained to the plugin — the framework seam still calls
updateAgentContext() unconditionally and the provider decides to no-op.

* docs(voice-assemblyai): use the model's name, Universal 3.5 Pro Realtime

Rename prose references from "Universal-3.5 Pro Streaming" to the official
"Universal 3.5 Pro Realtime" ac…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant